package cn.walls1717.wallsmall.product.service.impl;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.walls1717.wallsmall.product.dao.CategoryDao;
import cn.walls1717.wallsmall.product.entity.CategoryEntity;
import cn.walls1717.wallsmall.product.service.CategoryBrandRelationService;
import cn.walls1717.wallsmall.product.service.CategoryService;
import cn.walls1717.wallsmall.product.vo.Catelog2Vo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Resource
    private CategoryBrandRelationService categoryBrandRelationService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redisson;

    /**
     * 树形展示商品三级分类
     *
     * @return 商品分类集合
     */
    @Override
    public List<CategoryEntity> listCategoriesWithTree() {
        // 获取所有分类
        List<CategoryEntity> categories = list();
        return listOneLevelCategories().stream()
            // 设置分类的子分类
            .peek((root) -> root.setChildren(getChildren(root, categories)))
            // 根据sort字段升序排序
            .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
            // 收集为集合
            .collect(Collectors.toList());
    }

    /**
     * 找到catelogId的完整路径 [父/子/孙]
     *
     * @param catelogId 类别id
     * @return 层级完整路径
     */
    @Override
    public Long[] getCatalogPath(Long catelogId) {
        ArrayList<Long> paths = new ArrayList<>();
        List<Long> path = getParentPath(catelogId, paths);
        // 反转集合 [孙/子/父] -> [父/子/孙]
        Collections.reverse(path);
        return path.toArray(new Long[0]);
    }

    /**
     * 修改分类信息同时修改相关联的所有信息，保证冗余字段的数据一致性
     *
     * @param category 分类参数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateDetail(CategoryEntity category) {
        // 先更新本身category表
        updateById(category);
        String categoryName = category.getName();
        if (!StrUtil.isEmpty(categoryName)) {
            categoryBrandRelationService.updateCategoryByCatId(category.getCatId(), categoryName);
            // TODO 更新其他冗余字段
        }
    }

    /**
     * 获取一级分类
     *
     * @return 一级分类
     */
    @Override
    @Cacheable(value = "category", key = "'oneLevelCategories'")
    public List<CategoryEntity> listOneLevelCategories() {
        log.info("获取一级分类被调用");
        return lambdaQuery().eq(CategoryEntity::getCatLevel, 1).list();
    }

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        return getCatalogJsonWithRedisson();
    }

    /**
     * 使用Redisson简化使用Redis分布式锁
     *
     * @return catalogJson
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisson() {
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (catalogJson != null) {
            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
        }
        RLock lock = redisson.getLock("catalogJson-lock");
        lock.lock();
        Map<String, List<Catelog2Vo>> catalogJsonFromDb;
        try {
            catalogJsonFromDb = getCatalogFromDb();
            stringRedisTemplate.opsForValue().set("catalogJson", JSON.toJSONString(catalogJsonFromDb));
        } finally {
            lock.unlock();
        }
        return catalogJsonFromDb;
    }

    /**
     * <p>
     * 缓存击穿：一个热点key突然失效，大量的请求瞬间打到数据库上，这就是缓存击穿问题。
     * 
     * <p>
     * 可以通过加锁来解决，分布式系统下要考虑分布式锁，不然只会锁住一个服务器实例的请求，还是会有多个请求打到数据库。
     * 
     * @return catalogJson
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithLocalLock() {
        // 先从Redis中获取数据，若能获取到，直接返回
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (catalogJson != null) {
            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
        }

        // 从Redis中获取不到数据，需要加锁，解决缓存击穿问题
        synchronized (this) {
            /*
             双重校验：线程获得锁后仍然要判断缓存中是否有数据，有数据直接返回。
             如果没有双重校验，高并发下大量请求打进来，线程1竞争到锁，查询了数据库，写入了Redis，
             线程2再次获取锁，仍然会操作一遍数据库，这里加入判断保证后获取锁的线程不会再走数据库。
             */
            String json = stringRedisTemplate.opsForValue().get("catalogJson");
            if (json != null) {
                return JSON.parseObject(json, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
            }
            // 从数据库中拿到数据
            Map<String, List<Catelog2Vo>> collect = getCatalogFromDb();
            String jsonStr = JSON.toJSONString(collect);
            /*
            将数据保存到Redis中，这里必须将写入Redis的操作放到锁内，不然会出现时序性问题。
            时序问题(写入Redis在锁外的情况)：比如线程1从数据库中获取到了数据，释放了锁，在写入Redis之前，
            线程2获得锁，线程2理所应当的判断Redis中没有数据，就会再次操作数据库，造成锁失效。
             */
            stringRedisTemplate.opsForValue().set("catalogJson", jsonStr);
            return collect;
        }

    }

    /**
     * <p>
     * 使用Redis作为分布式锁，解决分布式系统下缓存击穿的问题
     * </p>
     * <p>
     * 这里需要注意两个关键点：加锁时的原子性，删除锁时的原子性
     * </p>
     *
     * @return catalogJson
     */
    private Map<String, List<Catelog2Vo>> getCatalogFromWithRedisLock() {
        // 先从Redis中获取数据，若能获取到，直接返回
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (catalogJson != null) {
            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
        }
        /*
         Redis中没有数据，加上Redis的NX锁
         这里的value使用uuid，保证每一个线程都有自己唯一的锁，
         避免出现误删其它线程的锁出现
         */
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 3000, TimeUnit.SECONDS);
        // lock为true表示加锁成功
        if (BooleanUtil.isTrue(lock)) {
            String catalogJson2 = stringRedisTemplate.opsForValue().get("catalogJson");
            if (catalogJson2 != null) {
                return JSON.parseObject(catalogJson2, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
            }
            Map<String, List<Catelog2Vo>> catalogFromDb;
            // 使用try-finally，业务代码就算有错误，也要正常释放锁，防止发生死锁
            try {
                catalogFromDb = getCatalogFromDb();
                String jsonStr = JSON.toJSONString(catalogFromDb);
                stringRedisTemplate.opsForValue().set("catalogJson", jsonStr);
            } finally {
                /*
                 使用lua脚本保证删除锁的原子性
                 lua脚本先根据key->local拿到value->uuid，比对uuid是否与传入的uuid一致，
                 如果一致，则执行删除操作，保证删除的是自己的锁，同时也保证原子性
                 */
                String script =
                    "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                    Collections.singletonList("lock"), uuid);
            }
            return catalogFromDb;
        }
        // 锁获取失败，睡眠200ms，自旋重复获取锁
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getCatalogFromWithRedisLock();
    }

    private Map<String, List<Catelog2Vo>> getCatalogFromDb() {
        List<CategoryEntity> categories = list();
        // 查出所有一级分类
        List<CategoryEntity> oneLevelCategories = getParentByCid(categories, 0L);

        // 封装数据
        Map<String, List<Catelog2Vo>> collect =
            oneLevelCategories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                // 1、每一个的一级分类,查到这个一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParentByCid(categories, v.getCatId());

                // 2、封装上面的结果
                List<Catelog2Vo> catelog2Vos = null;
                if (categoryEntities != null) {
                    catelog2Vos = categoryEntities.stream().map(l2 -> {
                        Catelog2Vo catelog2Vo =
                            new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                        // 1、找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catelog = getParentByCid(categories, l2.getCatId());

                        if (level3Catelog != null) {
                            List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
                                // 2、封装成指定格式
                                return new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(),
                                    l3.getName());
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(category3Vos);
                        }

                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                assert catelog2Vos != null;
                return catelog2Vos;
            }));
        return collect;
    }

    /**
     * 获取上级catelogId
     *
     * @param catelogId 类别id
     * @param paths 存放路径的集合
     * @return 类别层级id完整路径
     */
    private List<Long> getParentPath(Long catelogId, List<Long> paths) {
        // 先将当前id存入集合
        paths.add(catelogId);
        // 根据当前id查询到分类详情数据
        CategoryEntity category = this.getById(catelogId);
        Long parentCid = category.getParentCid();
        // 如果有上级分类则递归查找
        if (parentCid != 0) {
            getParentPath(parentCid, paths);
        }
        return paths;
    }

    /**
     * 获取分类的子分类
     *
     * @param root 根分类
     * @param categories 所有分类
     * @return 树状分类
     */
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> categories) {
        return categories.stream()
            // 获取根分类的子分类
            .filter(category -> Objects.equals(category.getParentCid(), root.getCatId()))
            // 递归设置分类的子分类
            .peek(category -> category.setChildren(getChildren(category, categories)))
            // 根据sort字段升序排序
            .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
            // 收集为集合
            .collect(Collectors.toList());
    }

    /**
     * 获取指定分类集合的父分类
     * 
     * @param categories 指定分类集合
     * @param parentCid 父亲的cid
     * @return 指定分类集合的父亲分类集合
     */
    private List<CategoryEntity> getParentByCid(List<CategoryEntity> categories, Long parentCid) {
        return categories.stream().filter(category -> category.getParentCid().equals(parentCid))
            .collect(Collectors.toList());
    }
}